Layer (Effect)
code:ts
Layer<Out, Error, In>
Out
Error
構築中に発生しうるエラーの型
In
構築に必要な依存
命名規則
HogeLive
HogeTest
Layerを作る
Layerの合成
LayerとLayerからLayerを作る
Layerの良いところmrsekut.icon
重なった依存を楽に解決できる
注入すべきものを後から楽に変えられる
早すぎる抽象化を避けられる
つまり、ServiceのInterfaceと実体のペアを作る部分を行う
0個以上の別のServiceを利用したServiceを定義できる
注入する側、実装側のまとまり
実装用レイヤ、テスト用レイヤを作っておけば、それを入れ替えるだけで済む
今のところ完全に上位互換に見えるmrsekut.icon
GPT-4.icon
サービスの実装に必要な依存関係を隠蔽
実行時ではなく「構築時」に依存を解決
再利用・テスト・モック化がしやすくなる
💡 なぜLayerが必要?
たとえば次のような依存関係グラフがあるとします:
code:_
Config
└─> Logger
└─> Database
このとき、Databaseを定義するときに以下のように書いてしまうと…
code:ts
import { Effect, Context } from "effect"
// Declaring a tag for the Config service
class Config extends Context.Tag("Config")<Config, {}>() {}
// Declaring a tag for the Logger service
class Logger extends Context.Tag("Logger")<Logger, {}>() {}
// Declaring a tag for the Database service
class Database extends Context.Tag("Database")<
Database,
{
// ❌ Avoid exposing Config and Logger as a requirement
readonly query: (
sql: string
) => Effect.Effect<unknown, never, Config | Logger>
}
() {}
依存がサービスのAPIに漏れてしまう(leak)
テストで Database を使うだけなのに Config や Logger も必要になる
Layer を使えば、これを回避できます
なるほどmrsekut.icon
Layer定義の例
code:ts
// ConfigLive :: Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({ logLevel: "INFO", connection: "..." })
})
Layer<Config, never, never>の意味
Layerを構築するとConfig Serviceが生成される
Layerの構築には失敗しない
Layerには依存関係がない
code:ts
const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({ logLevel: "INFO", connection: "..." })
})
)
Logger Layer
Config Serviceに依存するLayerを定義する
code:ts
// LoggerLive :: Layer<Logger, never, Config>
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return {
log: (msg) => Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log([${logLevel}] ${msg})
})
}
}))
Database Layer
ConfigとLoggerに依存するLayer
code:ts
const DatabaseLive = Layer.effect(Database, Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(Executing query: ${sql})
const { connection } = yield* config.getConfig
return { result: Results from ${connection} }
})
}
}))
Layer が 正常に構築された場合に何かをする
たとえば、成功したログを出す用途などに使う
code:ts
Layer.tap((ctx) => Console.log(Succeeded with: ${ctx}))
Layer が 失敗したときに何かをする。
たとえば、エラー時のログや通知などに使います。
code:ts
Layer.tapError((err) => Console.log(Failed with: ${err}))
🧪 テスト用のモックを使う
code:ts
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content...")
})
実ファイルシステムを使わず、常に "File Content..." を返すモック。
Cache.DefaultWithoutDependencies に加えて、このモックを提供すればテスト可能。
🔁 サービスそのものをモックする
code:ts
const cache = new Cache({
lookup: () => Effect.succeed("Cache Content...")
})
const runnable = program.pipe(Effect.provideService(Cache, cache))
依存関係を差し替えるのではなく、Cache 自体を差し替える例。